Go 中的 goroutine 之間沒有父與子的關係,也就沒有子進程退出後的通知機制,goroutine 都是平行的被調度。
在開發的過程當中,可能會開多個 goroutine 來操作,今天議題是如何優雅讓子 goroutine 優雅退出
使用select + chan 方式溝通
但試想一個場景是主 goroutine 當中,又啟用子 goroutine,當主 gorutine 結束後要通知子 gorutine
這時要討論到 Context
Go 1.7 加入了新個package 為 context ,定義了 Context 類型,專用來簡化處理多個 goroutine 之間請求領域的數據、取消信號、截止時間等相關操作
package main
import (
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(closeChan chan struct{}) {
LOOP:
for {
select {
case <-closeChan:
break LOOP
default:
fmt.Println("still worker")
time.Sleep(time.Second)
}
}
wg.Done()
}
func main() {
var closeChan = make(chan struct{})
wg.Add(1)
go worker(closeChan)
time.Sleep(time.Second * 3)
closeChan <- struct{}{}
close(closeChan)
wg.Wait()
fmt.Println("close")
}
Context 為一個 interface 定義了四個需要實現的方法,具體如下
Go内置兩個函数可以創建 Context:
Background() 和 TODO(),分别返回一個實現了Context接口的 background 和 todo。
使用 WithCancel() 將傳入 context 進行複製返回 cancelCtx 和 CancelFunc
若 CancelFunc 被執行時,會觸發去執行通知每個 cancelCtx children 進行cancel()
詳見源碼中的406行
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func work2(ctx context.Context) {
defer wg.Done()
LOOP:
for {
select {
case <-ctx.Done():
break LOOP
default:
time.Sleep(1 * time.Second)
fmt.Println("work2 is working")
}
}
}
func work1(ctx context.Context) {
defer wg.Done()
LOOP:
for {
go work2(ctx)
select {
case <-ctx.Done():
break LOOP
default:
time.Sleep(1 * time.Second)
fmt.Println("work1 is working")
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go work1(ctx)
time.Sleep(3 * time.Second)
cancel()
wg.Wait()
}
將傳入的parent Context進行複製並將傳入的時間進行調整為截止時間,當時間到截止時間則會進行關閉,也可以就由返回的 CancelFunc 進行關閉
設置過期時間 1 秒後過期
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func worker(ctx context.Context) {
LOOP:
for {
select {
case <-ctx.Done():
break LOOP
default:
time.Sleep(time.Millisecond * 100)
fmt.Println("still working")
}
}
fmt.Println("worker done!")
wg.Done()
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
wg.Add(1)
go worker(ctx)
time.Sleep(time.Second * 5)
cancel()
wg.Wait()
fmt.Println("over")
}
將傳入的parent Context進行複製並將傳入的 key 及 value 進行關聯
由Context.Value() 獲取 value
package main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}